﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using PubSub.S3.Abstract.DistributeEventBus;
using PubSub.S3.Abstract.LocalEventBus;

namespace PubSub.S3.Abstract
{
    public abstract class EventBusAbstract : IEventBus
    {
        /// <inheritdoc/>
        public virtual IDisposable Subscribe<TEvent>(Func<TEvent, Task> action) where TEvent : EventDataAbstract
        {
            return Subscribe(typeof(TEvent), new ActionEventHandler<TEvent>(action));
        }

        /// <inheritdoc/>
        public virtual IDisposable Subscribe<TEvent, THandler>()
            where TEvent : EventDataAbstract
            where THandler : IEventHandler, new()
        {
            return Subscribe(typeof(TEvent), new TransientEventHandlerFactory<THandler>());
        }

        /// <inheritdoc/>
        public virtual IDisposable Subscribe(Type eventType, IEventHandler handler)
        {
            return Subscribe(eventType, new SingleInstanceHandlerFactory(handler));
        }

        /// <inheritdoc/>
        public virtual IDisposable Subscribe<TEvent>(IEventHandlerFactory factory) where TEvent : EventDataAbstract
        {
            return Subscribe(typeof(TEvent), factory);
        }

        /// <inheritdoc/>
        public abstract IDisposable Subscribe(Type eventType, IEventHandlerFactory factory);
        
        /// <inheritdoc/>
        public abstract void Unsubscribe<TEvent>(Func<TEvent, Task> action) where TEvent : class;

        /// <inheritdoc/>
        public virtual void Unsubscribe<TEvent>(ILocalEventHandler<TEvent> handler) where TEvent : class
        {
            Unsubscribe(typeof(TEvent), handler);
        }

        public abstract void Unsubscribe(Type eventType, IEventHandler handler);

        /// <inheritdoc/>
        public virtual void Unsubscribe<TEvent>(IEventHandlerFactory factory) where TEvent : class
        {
            Unsubscribe(typeof(TEvent), factory);
        }

        /// <inheritdoc/>
        public abstract void Unsubscribe(Type eventType, IEventHandlerFactory factory);

        /// <inheritdoc/>
        public virtual void UnsubscribeAll<TEvent>() where TEvent : class
        {
            UnsubscribeAll(typeof(TEvent));
        }

        /// <inheritdoc/>
        public abstract void UnsubscribeAll(Type eventType);

        /// <inheritdoc/>
        public Task PublishAsync<TEvent>(TEvent eventData) where TEvent : EventDataAbstract
        {
            return PublishAsync(typeof(TEvent), eventData);
        }

        /// <inheritdoc/>
        public virtual async Task PublishAsync(Type eventType, EventDataAbstract eventData)
        {
            await PublishToEventBusAsync(eventType, eventData);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="eventType"></param>
        /// <param name="eventData"></param>
        /// <returns></returns>
        protected abstract Task PublishToEventBusAsync(Type eventType, EventDataAbstract eventData);

        /// <inheritdoc/>
        public virtual async Task TriggerHandlersAsync(Type eventType, EventDataAbstract eventData)
        {
            var exceptions = new List<Exception>();

            await TriggerHandlersAsync(eventType, eventData, exceptions);

            if (exceptions.Any())
            {
                ThrowOriginalExceptions(eventType, exceptions);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="eventType"></param>
        /// <param name="eventData"></param>
        /// <param name="exceptions"></param>
        protected virtual async Task TriggerHandlersAsync(Type eventType, EventDataAbstract eventData, List<Exception> exceptions )
        {
            //await new SynchronizationContextRemover();

            foreach (var handlerFactories in GetHandlerFactories(eventType))
            {
                foreach (var handlerFactory in handlerFactories.EventHandlerFactories)
                {
                    await TriggerHandlerAsync(handlerFactory, handlerFactories.EventType, eventData, exceptions);
                }
            }
        }

       

        /// <summary>
        /// 
        /// </summary>
        /// <param name="eventType"></param>
        /// <param name="exceptions"></param>
        /// <exception cref="Exception"></exception>
        /// <exception cref="AggregateException"></exception>
        protected void ThrowOriginalExceptions(Type eventType, List<Exception> exceptions)
        {
            if (exceptions.Count == 1)
            {
                //exceptions[0].ReThrow();
                throw exceptions[0];
            }

            throw new AggregateException(
                "More than one error has occurred while triggering the event: " + eventType,
                exceptions
            );
        }

        protected abstract IEnumerable<EventTypeWithEventHandlerFactories> GetHandlerFactories(Type eventType);

        protected virtual async Task TriggerHandlerAsync(IEventHandlerFactory asyncHandlerFactory, Type eventType,
            EventDataAbstract eventData, List<Exception> exceptions)
        {
            using (var eventHandlerWrapper = asyncHandlerFactory.GetHandlerWarpper())
            {
                try
                {
                    var handlerType = eventHandlerWrapper.EventHandler.GetType();
                    if (IsAssignableToGenericType(handlerType, typeof(ILocalEventHandler<>)))
                    {
                        var method = typeof(ILocalEventHandler<>)
                            .MakeGenericType(eventType)
                            .GetMethod(
                                nameof(ILocalEventHandler<object>.HandleEventAsync),
                                new[] {eventType}
                            );

                        await ((Task) method.Invoke(eventHandlerWrapper.EventHandler, new[] {eventData}));
                    }
                    else if (IsAssignableToGenericType(handlerType, typeof(IDistributedEventHandler<>)))
                    {
                        var method = typeof(IDistributedEventHandler<>)
                            .MakeGenericType(eventType)
                            .GetMethod(
                                nameof(IDistributedEventHandler<object>.HandleEventAsync),
                                new[] {eventType}
                            );

                        await ((Task) method.Invoke(eventHandlerWrapper.EventHandler, new[] {eventData}));
                    }
                    else
                    {
                        throw new Exception("The object instance is not an event handler. Object type: " +
                                            handlerType.AssemblyQualifiedName);
                    }
                }
                catch (TargetInvocationException ex)
                {
                    exceptions.Add(ex.InnerException);
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                }
            }
        }

        protected class EventTypeWithEventHandlerFactories
        {
            public Type EventType { get; }

            public List<IEventHandlerFactory> EventHandlerFactories { get; }

            public EventTypeWithEventHandlerFactories(Type eventType, List<IEventHandlerFactory> eventHandlerFactories)
            {
                EventType = eventType;
                EventHandlerFactories = eventHandlerFactories;
            }
        }

        /// <summary>
        /// Checks whether <paramref name="givenType"/> implements/inherits <paramref name="genericType"/>.
        /// </summary>
        /// <param name="givenType">Type to check</param>
        /// <param name="genericType">Generic type</param>
        private static bool IsAssignableToGenericType(Type givenType, Type genericType)
        {
            var givenTypeInfo = givenType.GetTypeInfo();

            if (givenTypeInfo.IsGenericType && givenType.GetGenericTypeDefinition() == genericType)
            {
                return true;
            }

            foreach (var interfaceType in givenTypeInfo.GetInterfaces())
            {
                if (interfaceType.GetTypeInfo().IsGenericType &&
                    interfaceType.GetGenericTypeDefinition() == genericType)
                {
                    return true;
                }
            }

            if (givenTypeInfo.BaseType == null)
            {
                return false;
            }

            return IsAssignableToGenericType(givenTypeInfo.BaseType, genericType);
        }
    }
}